import os
import asyncio
import json
import cv2
import numpy as np
from aiohttp import web
from aiohttp_session import setup, get_session, session_middleware
from aiohttp_session.cookie_storage import EncryptedCookieStorage
from mss import mss
from aiortc import RTCPeerConnection, RTCSessionDescription
from aiortc.mediastreams import VideoStreamTrack
from av import VideoFrame
import time
import socket
import struct
import threading
import ssl
import tempfile
import cryptography
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.fernet import Fernet
import datetime
import pyautogui
import subprocess
import sys
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QFormLayout, 
                            QSpinBox, QLineEdit, QPushButton, QHBoxLayout, QCheckBox, 
                            QLabel, QFileDialog, QGroupBox, QGridLayout, QTextEdit)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont, QIntValidator

# ====== 全局参数（可修改） ======
TARGET_FPS = 30             # 目标帧率
MAX_WIDTH = 1600            # 超过此宽度则等比缩放

STUN_PORT = 54250           # STUN 服务端口
WEB_PORT = 55555            # Web 服务端口（HTTPS）

users = ("", "")            # (用户名, 密码)
enable_file_download = True
enable_file_upload = False
enable_video = True
enable_control = False
enable_ipv4 = True
enable_ipv6 = False
UPLOAD_FOLDER = ''
try:
    with open('config.txt','r') as config_file:
        config_text = config_file.read()
    exec(config_text)
except BaseException:
    print('读入参数错误，使用默认配置')
# ============================
STUN_HOST = "::"            # STUN 服务监听地址
WEB_HOST_V4 = "0.0.0.0"     # Web 服务 IPv4 地址
WEB_HOST_V6 = "::"          # Web 服务 IPv6 地址
screen_width, screen_height = pyautogui.size()
CURSOR = False               # 是否捕获鼠标
MONITOR_INDEX = 1           # mss().monitors[1] 是主屏
SECRET_KEY = Fernet.generate_key().decode()
# ============================

index_html = '''
<!doctype html>
<html lang="zh">
<head>
  <meta charset="utf-8" />
  <title>屏幕播放</title>
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <style>
    body {
      margin: 0;
      padding: 0;
      background: black;
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
    }
    video {
      max-width: 100%;
      max-height: 100%;
      object-fit: contain;
      background: black;
    }
    #click-area {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      z-index: 10;
    }
    #floating-button {
      position: absolute;
      top: 20px;
      left: 20px;
      width: 50px;
      height: 50px;
      background-color: #39c5bb;
      color: white;
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
      user-select: none;
      z-index: 20;
    }
    #floating-window {
      position: absolute;
      top: 80px;
      left: 20px;
      width: 200px;
      background-color: white;
      border: 1px solid #ccc;
      border-radius: 5px;
      padding: 10px;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
      display: none;
      z-index: 20;
    }
    #floating-window label {
      display: block;
      margin-bottom: 10px;
    }
    .window-button {
      width: 100%;
      padding: 8px;
      margin-bottom: 10px;
      background-color: #39c5bb;
      color: white;
      border: none;
      border-radius: 3px;
      cursor: pointer;
      text-align: center;
      transition: background-color 0.3s;
    }
    .window-button:hover {
      background-color: #2da49c;
    }
    .error-message {
      position: fixed;
      bottom: 20px;
      left: 20px;
      background-color: #ff4444;
      color: white;
      padding: 10px 15px;
      border-radius: 4px;
      display: none;
      z-index: 100;
    }
  </style>
</head>
<body>
  <video id="video" autoplay playsinline muted></video>
  <div id="click-area"></div>
  <div id="floating-button">设置</div>
  <div id="floating-window">
    <label>
      <input type="checkbox" id="toggle-right-click"> 左键变右键
    </label>
    <button class="window-button" id="file-manager-btn">文件管理</button>
  </div>
  <div class="error-message" id="errorMessage"></div>

  <script>
    const videoEl = document.getElementById('video');
    const errorMessage = document.getElementById('errorMessage');
    let pc;

    // 显示错误消息
    function showError(message) {
      errorMessage.textContent = message;
      errorMessage.style.display = 'block';
      setTimeout(() => {
        errorMessage.style.display = 'none';
      }, 5000);
    }

    // 视频相关逻辑保持不变，但修复了ICE服务器配置
    async function start() {
      if (pc) {
        pc.close();
        pc = null;
      }

      try {
        // 修复ICE服务器配置，使用默认端口或正确的端口号
        const iceServers = [];
        const port = window.location.port || (window.location.protocol === 'https:' ? 443 : 80);
        
        // 仅当不是默认端口时才添加端口号
        const stunUrl = `stun:${window.location.hostname}${port && (port !== '80' && port !== '443') ? `:${port}` : ''}`;
        iceServers.push({ urls: stunUrl });

        pc = new RTCPeerConnection({ iceServers });

        // 接收远端视频
        pc.addTransceiver('video', { direction: 'recvonly' });
        pc.ontrack = (ev) => {
          videoEl.srcObject = ev.streams[0];
        };

        const offer = await pc.createOffer();
        await pc.setLocalDescription(offer);

        const resp = await fetch('/offer', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ sdp: offer.sdp, type: offer.type })
        });
        
        if (!resp.ok) {
          throw new Error(`服务器响应错误: ${resp.status}`);
        }
        
        const answer = await resp.json();
        await pc.setRemoteDescription(answer);
      } catch (error) {
        console.error('初始化错误:', error);
        showError(`初始化失败: ${error.message}`);
      }
    }

    start();

    window.addEventListener("beforeunload", () => {
      if (pc) pc.close();
    });

    // 从2.html添加的键鼠映射和悬浮窗逻辑
    const clickArea = document.getElementById('click-area');
    const floatingButton = document.getElementById('floating-button');
    const floatingWindow = document.getElementById('floating-window');
    const toggleRightClick = document.getElementById('toggle-right-click');
    const fileManagerBtn = document.getElementById('file-manager-btn');

    // 文件管理按钮点击事件 - 跳转到当前URL的/file路径
    fileManagerBtn.addEventListener('click', () => {
      try {
        // 获取当前URL并添加/file路径
        const currentUrl = window.location.href;
        // 处理可能已存在的路径
        const baseUrl = currentUrl.split('/').slice(0, 3).join('/');
        const fileUrl = `${baseUrl}/file`;
        window.location.href = fileUrl;
      } catch (error) {
        console.error('文件管理跳转错误:', error);
        showError('无法打开文件管理');
      }
    });

    let isDragging = false;
    let offsetX, offsetY;

    // 屏蔽右键默认菜单
    clickArea.addEventListener('contextmenu', (event) => {
      event.preventDefault();
    });

    // 悬浮按钮点击事件
    floatingButton.addEventListener('click', () => {
      floatingWindow.style.display = floatingWindow.style.display === 'none' ? 'block' : 'none';
    });

    // 悬浮按钮拖动事件
    floatingButton.addEventListener('mousedown', (event) => {
      isDragging = true;
      offsetX = event.clientX - floatingButton.offsetLeft;
      offsetY = event.clientY - floatingButton.offsetTop;
    });

    document.addEventListener('mousemove', (event) => {
      if (isDragging) {
        floatingButton.style.left = `${event.clientX - offsetX}px`;
        floatingButton.style.top = `${event.clientY - offsetY}px`;
        floatingWindow.style.left = `${event.clientX - offsetX}px`;
        floatingWindow.style.top = `${event.clientY - offsetY + 60}px`;
      }
    });

    document.addEventListener('mouseup', () => {
      isDragging = false;
    });

    // 根据视频的渲染尺寸计算有效点击区域，并返回百分比坐标
    function getVideoCoordinates(event) {
      const video = document.querySelector('video');
      const rect = video.getBoundingClientRect();
      const naturalWidth = video.videoWidth;
      const naturalHeight = video.videoHeight;
      const displayWidth = rect.width;
      const displayHeight = rect.height;
      
      const videoAspectRatio = naturalWidth / naturalHeight;
      const displayAspectRatio = displayWidth / displayHeight;
      
      let renderWidth, renderHeight, offsetX, offsetY;
      
      if (displayAspectRatio > videoAspectRatio) {
        renderHeight = displayHeight;
        renderWidth = renderHeight * videoAspectRatio;
        offsetX = (displayWidth - renderWidth) / 2;
        offsetY = 0;
      } else {
        renderWidth = displayWidth;
        renderHeight = renderWidth / videoAspectRatio;
        offsetX = 0;
        offsetY = (displayHeight - renderHeight) / 2;
      }
      
      const x = event.clientX - rect.left - offsetX;
      const y = event.clientY - rect.top - offsetY;
      
      if (x < 0 || x > renderWidth || y < 0 || y > renderHeight) {
        return null;
      }
      
      const xPercent = (x / renderWidth) * 100;
      const yPercent = (y / renderHeight) * 100;
      
      return { xPercent, yPercent };
    }
    
    // 用于记录鼠标按下时的起始坐标
    let dragStart = null;
    
    // 鼠标按下时记录起始坐标
    clickArea.addEventListener('mousedown', (event) => {
      event.preventDefault();
      const coords = getVideoCoordinates(event);
      if (!coords) return;
      const button = toggleRightClick.checked ? 'right' : (event.button === 2 ? 'right' : 'left');
      dragStart = { x: coords.xPercent, y: coords.yPercent, button };
    });
    
    // 鼠标松开时发送拖动请求
    clickArea.addEventListener('mouseup', async (event) => {
      event.preventDefault();
      if (!dragStart) return;
      const coords = getVideoCoordinates(event);
      if (!coords) {
        dragStart = null;
        return;
      }
      
      try {
        const response = await fetch('/drag', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            start_x: dragStart.x,
            start_y: dragStart.y,
            end_x: coords.xPercent,
            end_y: coords.yPercent,
            button: dragStart.button
          }),
        });
        
        if (!response.ok) {
          throw new Error(`服务器未找到此接口: ${response.status}`);
        }
      } catch (error) {
        console.error('拖动请求错误:', error);
        showError('操作无法执行: 服务器不支持此功能');
      } finally {
        dragStart = null;
      }
    });
    
    // 键盘事件映射
    document.addEventListener('keydown', async (event) => {
      const key = event.key;
      try {
        const response = await fetch('/keypress', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({ key }),
        });
        
        if (!response.ok) {
          throw new Error(`服务器未找到此接口: ${response.status}`);
        }
      } catch (error) {
        console.error('键盘事件错误:', error);
        showError('操作无法执行: 服务器不支持此功能');
      }
    });
    
    // 窗口大小变化时重置视频尺寸
    window.addEventListener('resize', () => {
      const video = document.querySelector('video');
      if (video) {
        video.style.maxWidth = '100%';
        video.style.maxHeight = '100%';
      }
    });
  </script>
</body>
</html>
'''

login_html = '''
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>用户登录</title>
    <style>
        body {{
            font-family: Arial, sans-serif;
            background-color: #f0f2f5;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
        }}
        .login-container {{
            background-color: #ffffff;
            padding: 40px 50px;
            border-radius: 12px;
            box-shadow: 0 8px 20px rgba(0,0,0,0.1);
            width: 350px;
            text-align: center;
        }}
        .login-container h2 {{
            margin-bottom: 30px;
            color: #333333;
        }}
        .login-container input[type="text"],
        .login-container input[type="password"] {{
            width: 100%;
            padding: 12px 15px;
            margin: 10px 0;
            border: 1px solid #ddd;
            border-radius: 6px;
            box-sizing: border-box;
            font-size: 14px;
        }}
        .login-container button {{
            width: 100%;
            padding: 12px 15px;
            margin-top: 20px;
            background-color: #39c5bb;
            border: none;
            border-radius: 6px;
            color: white;
            font-size: 16px;
            cursor: pointer;
            transition: background-color 0.3s;
        }}
        .login-container button:hover {{
            background-color: #80dbd5;
        }}
        .error-message {{
            background-color: #fff0f0;
            border: 1px solid #ffcccc;
            color: #d8000c;
            padding: 12px 15px;
            border-radius: 6px;
            margin-bottom: 20px;
            font-size: 14px;
            display: {error_display};
        }}
    </style>
</head>
<body>
    <div class="login-container">
        <h2>隧  影  投  屏</h2>
        <form method="post" action="/login">
            <div class="error-message">{error_message}</div>
            <input type="text" name="账户名" placeholder="账户名">
            <input type="password" name="密码" placeholder="密码">
            <button type="submit">登录</button>
        </form>
    </div>
</body>
</html>
'''

file_html = '''
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>文件管理器</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }

        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            line-height: 1.6;
            color: #333;
            max-width: 800px;
            margin: 0 auto;
            padding: 30px 20px;
            background-color: #f8f9fa;
        }

        /* 返回按钮样式 */
        .back-button {
            position: absolute;
            top: 30px;
            left: 20px;
            background-color: #95a5a6;
            color: white;
            border: none;
            padding: 8px 16px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 0.9rem;
            text-decoration: none;
            transition: background-color 0.3s ease;
        }

        .back-button:hover {
            background-color: #7f8c8d;
        }

        h1 {
            color: #2c3e50;
            margin-bottom: 30px;
            padding-bottom: 10px;
            border-bottom: 2px solid #3498db;
            text-align: center;
        }

        h2 {
            color: #34495e;
            margin: 25px 0 15px;
            font-size: 1.4rem;
        }

        .upload-form {
            background-color: white;
            padding: 25px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            margin-bottom: 30px;
            transition: box-shadow 0.3s ease;
        }

        .upload-form:hover {
            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
        }

        input[type="file"] {
            display: block;
            margin-bottom: 15px;
            padding: 8px;
            border: 1px solid #ddd;
            border-radius: 4px;
            width: 100%;
            cursor: pointer;
        }

        input[type="submit"] {
            background-color: #3498db;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 1rem;
            transition: background-color 0.3s ease;
        }

        input[type="submit"]:hover {
            background-color: #2980b9;
        }

        ul {
            list-style-type: none;
            background-color: white;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            overflow: hidden;
        }

        .file-item {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 12px 20px;
            border-bottom: 1px solid #f1f1f1;
        }

        .file-link {
            color: #2980b9;
            text-decoration: none;
            transition: all 0.3s ease;
        }

        .file-link:hover {
            color: #1a5276;
            text-decoration: underline;
        }

        .delete-btn {
            margin-left: 10px;
            background-color: #e74c3c;
            color: white;
            border: none;
            padding: 3px 8px;
            border-radius: 3px;
            cursor: pointer;
            font-size: 0.8rem;
            transition: background-color 0.3s ease;
        }

        .delete-btn:hover {
            background-color: #c0392b;
        }

        /* 进度条样式 */
        .progress-container {
            width: 100%;
            height: 8px;
            background-color: #e0e0e0;
            border-radius: 4px;
            margin-top: 15px;
            overflow: hidden;
            display: none; /* 默认隐藏 */
        }

        .progress-bar {
            height: 100%;
            background-color: #3498db;
            width: 0%;
            transition: width 0.3s ease;
        }

        /* 状态提示样式 */
        .status-message {
            margin-top: 15px;
            padding: 10px;
            border-radius: 4px;
            display: none;
        }

        .success {
            background-color: #dff0d8;
            color: #3c763d;
            border: 1px solid #d6e9c6;
        }

        .error {
            background-color: #f2dede;
            color: #a94442;
            border: 1px solid #ebccd1;
        }

        /* 空列表提示 */
        .empty-message {
            padding: 12px 20px;
            color: #7f8c8d;
            text-align: center;
        }
    </style>
</head>
<body>
    <!-- 添加返回按钮 -->
    <a href="../" class="back-button">返回</a>
    
    <h1>文件管理器</h1>

    <h2>上传文件</h2>
    <form id="uploadForm" class="upload-form" method="POST" enctype="multipart/form-data">
        <input type="file" name="file" id="fileInput" multiple>
        <input type="submit" value="上传">
        <div class="progress-container">
            <div class="progress-bar" id="progressBar"></div>
        </div>
        <div class="status-message" id="statusMessage"></div>
    </form>

    <h2>文件列表</h2>
    <ul id="fileList">
        <!-- 文件列表通过JavaScript动态生成 -->
        <li class="empty-message">加载文件列表中...</li>
    </ul>

    <script>
        // 页面加载时就获取文件列表
        document.addEventListener('DOMContentLoaded', function() {
            refreshFileList();
        });

        // 文件上传进度
        const uploadForm = document.getElementById('uploadForm');
        const progressBar = document.getElementById('progressBar');
        const progressContainer = document.querySelector('.progress-container');
        const statusMessage = document.getElementById('statusMessage');
        const fileList = document.getElementById('fileList');

        uploadForm.addEventListener('submit', function(e) {
            e.preventDefault();
            
            const formData = new FormData(this);
            const xhr = new XMLHttpRequest();
            
            xhr.open('POST', '/upload', true);
            
            xhr.upload.addEventListener('progress', function(e) {
                if (e.lengthComputable) {
                    const percent = (e.loaded / e.total) * 100;
                    progressBar.style.width = percent + '%';
                    progressContainer.style.display = 'block';
                }
            });
            
            xhr.addEventListener('load', function() {
                progressContainer.style.display = 'none';
                if (xhr.status === 200) {
                    showStatus('文件上传成功！', 'success');
                    refreshFileList();
                } else {
                    showStatus('上传失败: ' + xhr.responseText, 'error');
                }
                // 重置表单
                uploadForm.reset();
                progressBar.style.width = '0%';
            });
            
            xhr.addEventListener('error', function() {
                progressContainer.style.display = 'none';
                showStatus('上传失败，请重试', 'error');
            });
            
            xhr.send(formData);
        });

        // 刷新文件列表
        function refreshFileList() {
            fetch('/file', {
                headers: { "Accept": "application/json" }
            })
            .then(response => response.json())
            .then(files => {
                fileList.innerHTML = '';
                if (!files || files.length === 0) {
                    const emptyItem = document.createElement('li');
                    emptyItem.className = 'empty-message';
                    emptyItem.textContent = '暂无文件';
                    fileList.appendChild(emptyItem);
                    return;
                }
                files.forEach(file => {
                    const listItem = document.createElement('li');
                    listItem.className = 'file-item';
                    
                    const fileLink = document.createElement('a');
                    fileLink.href = '/download/' + encodeURIComponent(file);
                    fileLink.className = 'file-link';
                    fileLink.textContent = file;
                    fileLink.setAttribute('download', file);
                    
                    const deleteBtn = document.createElement('button');
                    deleteBtn.className = 'delete-btn';
                    deleteBtn.textContent = '删除';
                    deleteBtn.setAttribute('data-filename', file);
                    
                    listItem.appendChild(fileLink);
                    listItem.appendChild(deleteBtn);
                    fileList.appendChild(listItem);
                });
            })
            .catch(error => {
                console.error('刷新文件列表失败:', error);
                fileList.innerHTML = '<li class="empty-message">加载文件失败</li>';
            });
        }

        // 显示状态消息
        function showStatus(message, type) {
            statusMessage.textContent = message;
            statusMessage.className = 'status-message ' + type;
            statusMessage.style.display = 'block';
            
            // 3秒后隐藏消息
            setTimeout(() => {
                statusMessage.style.display = 'none';
            }, 3000);
        }

        // 为删除按钮添加事件监听
        fileList.addEventListener('click', function(e) {
            if (e.target.classList.contains('delete-btn')) {
                const filename = e.target.getAttribute('data-filename');
                if (confirm(`确定要删除文件 "${filename}" 吗？`)) {
                    deleteFile(filename);
                }
            }
        });

        // 删除文件函数
        function deleteFile(filename) {
            fetch(`/delete/${encodeURIComponent(filename)}`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                }
            })
            .then(response => response.json())
            .then(data => {
                if (data.status === 'success') {
                    showStatus('文件删除成功！', 'success');
                    refreshFileList();
                } else {
                    showStatus('删除失败: ' + data.message, 'error');
                }
            })
            .catch(error => {
                console.error('删除文件失败:', error);
                showStatus('删除失败，请重试', 'error');
            });
        }
    </script>
</body>
</html>
'''


class ScreenTrack(VideoStreamTrack):
    kind = "video"

    def __init__(self, monitor_index=1, fps=20, max_width=1280, cursor=True):
        super().__init__()
        self.sct = mss()
        self.monitor = self.sct.monitors[monitor_index]
        self.frame_interval = 1.0 / fps
        self.max_width = max_width
        self.cursor = cursor
        self._last_ts = time.time()

    async def recv(self) -> VideoFrame:
        now = time.time()
        sleep_time = self.frame_interval - (now - self._last_ts)
        if sleep_time > 0:
            await asyncio.sleep(sleep_time)
        self._last_ts = time.time()

        raw = self.sct.grab(self.monitor)
        img = np.asarray(raw)  # (H, W, 4) BGRA
        frame_bgr = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)

        h, w = frame_bgr.shape[:2]
        if w > self.max_width:
            scale = self.max_width / w
            frame_bgr = cv2.resize(frame_bgr, (int(w*scale), int(h*scale)), interpolation=cv2.INTER_AREA)

        av_frame = VideoFrame.from_ndarray(frame_bgr, format="bgr24")
        av_frame = av_frame.reformat(format="yuv420p")

        pts, time_base = await self.next_timestamp()
        av_frame.pts = pts
        av_frame.time_base = time_base
        return av_frame


routes = web.RouteTableDef()
pcs = set()

# ===================主页面接口=====================
@routes.get("/")
async def index(request):
    session = await get_session(request)
    if 'username' not in session:
        raise web.HTTPFound('/login')
    if not enable_video:
        raise web.HTTPFound('/file')
    
    return web.Response(
        text=index_html,
        content_type="text/html"
    )

@routes.route('*', '/login')
async def login(request):
    """登录视图，处理GET和POST请求"""
    error_message = ""
    
    if request.method == 'POST':
        # 解析表单数据
        data = await request.post()
        username = data.get('账户名', '').strip()
        password = data.get('密码', '').strip()
        
        if username == users[0] and password == users[1]:
            session = await get_session(request)
            session['username'] = username
            raise web.HTTPFound('/')
        else:
            error_message = "用户名或密码错误，请重新输入"
    
    # 格式化 HTML，填入错误提示和显示状态
    return web.Response(
        text=login_html.format(
            error_message=error_message,
            error_display="block" if error_message else "none"
        ),
        content_type='text/html'
    )


@routes.post("/offer")
async def offer(request):
    params = await request.json()
    offer = RTCSessionDescription(sdp=params["sdp"], type=params["type"])

    pc = RTCPeerConnection()
    pcs.add(pc)

    @pc.on("connectionstatechange")
    async def on_state_change():
        print("Connection state:", pc.connectionState)
        if pc.connectionState in ("failed", "closed", "disconnected"):
            await pc.close()
            pcs.discard(pc)

    screen_track = ScreenTrack(
        monitor_index=MONITOR_INDEX,
        fps=TARGET_FPS,
        max_width=MAX_WIDTH,
        cursor=CURSOR
    )
    pc.addTrack(screen_track)

    await pc.setRemoteDescription(offer)
    answer = await pc.createAnswer()
    await pc.setLocalDescription(answer)

    return web.Response(
        content_type="application/json",
        text=json.dumps({"sdp": pc.localDescription.sdp, "type": pc.localDescription.type})
    )


@routes.get("/health")
async def health(_):
    session = await get_session(request)
    if 'username' not in session:
        raise web.HTTPFound('/login')
    return web.json_response({"ok": True, "peers": len(pcs)})


# 远程控制映射接口
@routes.post("/click")
async def handle_click(request):
    session = await get_session(request)
    if 'username' not in session:
        raise web.HTTPFound('/login')
    if not enable_control:
        return web.json_response({"status": "disabled"}, status=403)
    
    data = await request.json()
    x_percent = data['x']
    y_percent = data['y']
    button = data.get('button', 'left')

    actual_x = int((x_percent / 100) * screen_width)
    actual_y = int((y_percent / 100) * screen_height)

    if button == 'right':
        pyautogui.rightClick(actual_x, actual_y)
    else:
        pyautogui.click(actual_x, actual_y)
    
    return web.json_response({"status": "success"}, status=200) 

@routes.post("/drag")
async def handle_drag(request):
    session = await get_session(request)
    if 'username' not in session:
        raise web.HTTPFound('/login')
    if not enable_control:
        return web.json_response({"status": "disabled"}, status=403)
    
    data = await request.json()
    start_x_percent = data['start_x']
    start_y_percent = data['start_y']
    end_x_percent = data['end_x']
    end_y_percent = data['end_y']
    button = data.get('button', 'left')
    
    actual_start_x = int((start_x_percent / 100) * screen_width)
    actual_start_y = int((start_y_percent / 100) * screen_height)
    actual_end_x = int((end_x_percent / 100) * screen_width)
    actual_end_y = int((end_y_percent / 100) * screen_height)
    
    pyautogui.moveTo(actual_start_x, actual_start_y)
    pyautogui.mouseDown(button=button)
    time.sleep(0.3)
    pyautogui.moveTo(actual_end_x, actual_end_y, duration=0.05)
    pyautogui.mouseUp(button=button)
    
    return web.json_response({"status": "success"}, status=200)

@routes.post("/keypress")
async def handle_keypress(request):
    session = await get_session(request)
    if 'username' not in session:
        raise web.HTTPFound('/login')
    if not enable_control:
        return web.json_response({"status": "disabled"}, status=403)
    
    data = await request.json()
    key = data['key']
    pyautogui.press(key)
    return web.json_response({"status": "success"}, status=200)  # 200表示成功

# ========================文件传输接口========================
@routes.get('/file')
async def fileindex(request):
    session = await get_session(request)
    if 'username' not in session:
        raise web.HTTPFound('/login')

    if (not (enable_file_download or enable_file_upload)) or (UPLOAD_FOLDER == ''):
        return web.Response(text="服务端未允许文件传输", status=403)

    # 判断请求是否是 Ajax/json 请求
    accept = request.headers.get("Accept", "")
    if "application/json" in accept:
        files = os.listdir(UPLOAD_FOLDER)
        return web.json_response(files)
    else:
        return web.Response(text=file_html, content_type="text/html")

@routes.post('/upload')
async def upload_file(request):
    session = await get_session(request)
    if (not enable_file_upload) or ('username' not in session) or (UPLOAD_FOLDER == ''):
        return web.json_response({"status": "error", "message": "服务端未允许文件传输"}, status=403)

    reader = await request.multipart()
    field = await reader.next()
    if field is None or field.name != "file":
        return web.json_response({"status": "error", "message": "未找到文件"}, status=400)

    filename = field.filename
    if not filename:
        return web.json_response({"status": "error", "message": "文件名不能为空"}, status=400)

    file_path = os.path.join(UPLOAD_FOLDER, filename)
    try:
        with open(file_path, 'wb') as f:
            while True:
                chunk = await field.read_chunk(1024 * 1024)
                if not chunk:
                    break
                f.write(chunk)
        return web.json_response({"status": "success", "message": "文件上传成功"})
    except Exception as e:
        return web.json_response({"status": "error", "message": f"上传失败: {str(e)}"}, status=500)

@routes.get('/download/{filename}')
async def download_file(request):
    session = await get_session(request)
    if (not enable_file_download) or ('username' not in session) or (UPLOAD_FOLDER == ''):
        return web.Response(text="服务端未允许文件传输", status=403)

    filename = request.match_info['filename']
    file_path = os.path.join(UPLOAD_FOLDER, filename)
    if not os.path.exists(file_path):
        return web.Response(text="文件不存在", status=404)

    return web.FileResponse(path=file_path)

@routes.post('/delete/{filename}')
async def delete_file(request):
    session = await get_session(request)
    if (not enable_file_upload) or ('username' not in session) or (UPLOAD_FOLDER == ''):
        return web.json_response({"status": "error", "message": "没有删除权限"}, status=403)

    filename = request.match_info['filename']
    file_path = os.path.join(UPLOAD_FOLDER, filename)

    try:
        if not os.path.abspath(file_path).startswith(os.path.abspath(UPLOAD_FOLDER)):
            return web.json_response({"status": "error", "message": "无效的文件路径"}, status=400)

        if not os.path.exists(file_path) or not os.path.isfile(file_path):
            return web.json_response({"status": "error", "message": "文件不存在"}, status=404)

        os.remove(file_path)
        return web.json_response({"status": "success", "message": "文件已删除"})
    except Exception as e:
        return web.json_response({"status": "error", "message": f"删除失败: {str(e)}"}, status=500)

@routes.get('/file-list')
async def file_list(request):
    session = await get_session(request)
    if (not (enable_file_download or enable_file_upload)) or ('username' not in session) or (UPLOAD_FOLDER == ''):
        return web.json_response([], status=403)

    try:
        files = [f for f in os.listdir(UPLOAD_FOLDER) if os.path.isfile(os.path.join(UPLOAD_FOLDER, f))]
        return web.json_response(files)
    except Exception:
        return web.json_response([], status=500)



# 关闭网页服务
async def on_shutdown(app):
    coros = [pc.close() for pc in pcs]
    await asyncio.gather(*coros, return_exceptions=True)
    pcs.clear()


# ========== 简易 STUN 服务器 ==========
def run_stun_server(host=STUN_HOST, port=STUN_PORT):
    sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    try:
        sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
    except OSError:
        pass
    sock.bind((host, port))
    print(f"STUN server listening on [{host}]:{port}")

    while True:
        data, addr = sock.recvfrom(2048)
        if len(data) < 20:
            continue

        msg_type, msg_len, magic_cookie = struct.unpack("!HHI", data[:8])
        if magic_cookie != 0x2112A442:
            continue
        if msg_type != 0x0001:
            continue

        trans_id = data[8:20]
        attrs = b""
        ip_str, portnum = addr[0], addr[1]

        try:
            ip_bytes = socket.inet_pton(socket.AF_INET, ip_str)
            x_port = portnum ^ (0x2112)
            x_ip = struct.unpack("!I", ip_bytes)[0] ^ 0x2112A442
            attr = struct.pack("!HHBBH", 0x0020, 8, 0, 0x01, x_port) + struct.pack("!I", x_ip)
            attrs += attr
        except OSError:
            try:
                ip_bytes = socket.inet_pton(socket.AF_INET6, ip_str)
                x_port = portnum ^ (0x2112)
                x_ip = bytearray(ip_bytes)
                cookie = struct.pack("!I", 0x2112A442) + trans_id
                for i in range(16):
                    x_ip[i] ^= cookie[i % len(cookie)]
                attr = struct.pack("!HHBBH", 0x0020, 20, 0, 0x02, x_port) + bytes(x_ip)
                attrs += attr
            except OSError:
                pass

        if not attrs:
            continue

        msg_type = 0x0101
        msg_len = len(attrs)
        header = struct.pack("!HHI", msg_type, msg_len, 0x2112A442) + trans_id
        resp = header + attrs
        sock.sendto(resp, addr)


# ========== 生成临时 HTTPS 证书 ==========
def generate_ssl_context():
    cert_file = "server.crt"
    key_file = "server.key"

    # 如果证书和私钥已经存在，直接加载
    if os.path.exists(cert_file) and os.path.exists(key_file):
        ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
        ssl_ctx.load_cert_chain(cert_file, key_file)
        print(f"Loaded existing certificate: {cert_file}, {key_file}")
        return ssl_ctx

    # 生成新的证书和私钥
    key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
    subject = issuer = x509.Name([
        x509.NameAttribute(NameOID.COMMON_NAME, u"localhost")
    ])
    cert = (
        x509.CertificateBuilder()
        .subject_name(subject)
        .issuer_name(issuer)
        .public_key(key.public_key())
        .serial_number(x509.random_serial_number())
        .not_valid_before(datetime.datetime.now(datetime.UTC) - datetime.timedelta(days=1))
        .not_valid_after(datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=365))  # 有效期1年
        .sign(key, hashes.SHA256())
    )

    # 写入当前目录
    with open(cert_file, "wb") as f:
        f.write(cert.public_bytes(serialization.Encoding.PEM))
    with open(key_file, "wb") as f:
        f.write(
            key.private_bytes(
                encoding=serialization.Encoding.PEM,
                format=serialization.PrivateFormat.TraditionalOpenSSL,
                encryption_algorithm=serialization.NoEncryption()
            )
        )

    ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
    ssl_ctx.load_cert_chain(cert_file, key_file)
    print(f"Generated new certificate: {cert_file}, {key_file}")
    return ssl_ctx

async def main_async():
    threading.Thread(target=run_stun_server, daemon=True).start()

    storage = EncryptedCookieStorage(
        SECRET_KEY,
        cookie_name="MYSESSION",
        max_age=None,
        httponly=True,
        secure=True
    )
    app = web.Application(middlewares=[session_middleware(storage)])
    setup(app, storage)
    app.add_routes(routes)
    app.on_shutdown.append(on_shutdown)

    runner = web.AppRunner(app)
    await runner.setup()

    ssl_ctx = generate_ssl_context()

    site_v4 = web.TCPSite(runner, host=WEB_HOST_V4, port=WEB_PORT, ssl_context=ssl_ctx)
    site_v6 = web.TCPSite(runner, host=WEB_HOST_V6, port=WEB_PORT, ssl_context=ssl_ctx)

    if enable_ipv4:
        await site_v4.start()
    if enable_ipv6:
        await site_v6.start()

    print(f"HTTPS Web server listening on {WEB_HOST_V4}:{WEB_PORT} and [{WEB_HOST_V6}]:{WEB_PORT}")

    while True:
        await asyncio.sleep(3600)


def main():
    asyncio.run(main_async())


# ============================UI界面==============================
class ConfigWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        font = QFont()
        font.setFamily("SimHei")
        font.setPointSize(10)
        self.setFont(font)
        self.initUI()
        
    def initUI(self):
        self.setWindowTitle('隧影投屏')
        self.setGeometry(100, 100, 700, 900)
        self.setFixedSize(700, 900)  # 锁定窗口大小
        
        central_widget = QWidget()
        central_widget.setStyleSheet("""
            QWidget { background-color: #f5f5f5; }
            QLabel { color: #333333; }
            QLabel#titleLabel {
                font-size: 16px;
                font-weight: bold;
                color: #2c3e50;
                margin: 10px 0;
            }
            QGroupBox {
                border: 1px solid #ddd;
                border-radius: 6px;
                margin-top: 10px;
                padding: 15px;
                background-color: white;
            }
            QGroupBox::title {
                subcontrol-origin: margin;
                left: 10px;
                padding: 0 5px 0 5px;
                color: #34495e;
                font-weight: bold;
            }
            QPushButton {
                background-color: #3498db;
                color: white;
                border: none;
                padding: 6px 12px;
                border-radius: 4px;
                font-size: 10px;
            }
            QPushButton:hover { background-color: #2980b9; }
            QPushButton:pressed { background-color: #2471a3; }
            QPushButton#startBtn { background-color: #2ecc71; }
            QPushButton#startBtn:hover { background-color: #27ae60; }
            QPushButton#startBtn:pressed { background-color: #219653; }
            QPushButton#browseBtn { background-color: #95a5a6; }
            QPushButton#browseBtn:hover { background-color: #7f8c8d; }
            QLineEdit, QCheckBox {
                padding: 5px;
                border: 1px solid #bdc3c7;
                border-radius: 3px;
                background-color: white;
            }
            QLineEdit:focus {
                border-color: #3498db;
                outline: none;
            }
        """)
        
        self.setCentralWidget(central_widget)
        main_layout = QVBoxLayout(central_widget)
        main_layout.setContentsMargins(20, 20, 20, 20)
        main_layout.setSpacing(15)
        
        
        # 视频参数分组
        video_group = QGroupBox("视频参数设置")
        video_layout = QFormLayout()
        video_layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
        video_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
        video_layout.setLabelAlignment(Qt.AlignLeft)
        video_layout.setVerticalSpacing(10)
        
        # 目标帧率 - 使用QLineEdit并添加整数验证
        self.fps_edit = QLineEdit()
        self.fps_edit.setText(str(TARGET_FPS))
        self.fps_edit.setValidator(QIntValidator(1, 120))  # 限制1-120之间的整数
        video_layout.addRow('目标帧率:', self.fps_edit)
        
        # 最大宽度 - 使用QLineEdit并添加整数验证
        self.width_edit = QLineEdit()
        self.width_edit.setText(str(MAX_WIDTH))
        self.width_edit.setValidator(QIntValidator(320, 4096))  # 限制320-4096之间的整数
        video_layout.addRow('最大宽度:', self.width_edit)
        
        video_group.setLayout(video_layout)
        main_layout.addWidget(video_group)
        
        # 网络参数分组
        network_group = QGroupBox("网络参数设置")
        network_layout = QFormLayout()
        network_layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
        network_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
        network_layout.setLabelAlignment(Qt.AlignLeft)
        network_layout.setVerticalSpacing(10)
        
        # STUN端口
        self.stun_port_edit = QLineEdit()
        self.stun_port_edit.setText(str(STUN_PORT))
        self.stun_port_edit.setValidator(QIntValidator(1, 65535))  # 限制1-65535之间的整数
        network_layout.addRow('STUN端口:', self.stun_port_edit)
        
        # Web端口
        self.web_port_edit = QLineEdit()
        self.web_port_edit.setText(str(WEB_PORT))
        self.web_port_edit.setValidator(QIntValidator(1, 65535))  # 限制1-65535之间的整数
        network_layout.addRow('Web端口:', self.web_port_edit)
        
        # IPv4/IPv6设置
        ip_layout = QHBoxLayout()
        self.ipv4_check = QCheckBox("启用IPv4")
        self.ipv4_check.setChecked(enable_ipv4)
        self.ipv6_check = QCheckBox("启用IPv6")
        self.ipv6_check.setChecked(enable_ipv6)
        ip_layout.addWidget(self.ipv4_check)
        ip_layout.addWidget(self.ipv6_check)
        ip_layout.addStretch()
        network_layout.addRow('网络协议:', ip_layout)
        
        network_group.setLayout(network_layout)
        main_layout.addWidget(network_group)
        
        # 认证与存储分组
        auth_group = QGroupBox("认证与存储设置")
        auth_layout = QFormLayout()
        auth_layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
        auth_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
        auth_layout.setLabelAlignment(Qt.AlignLeft)
        auth_layout.setVerticalSpacing(10)
        
        self.username_edit = QLineEdit()
        self.username_edit.setText(users[0])
        auth_layout.addRow('用户名:', self.username_edit)
        
        self.password_edit = QLineEdit()
        self.password_edit.setEchoMode(QLineEdit.Password)
        self.password_edit.setText(users[1])
        auth_layout.addRow('密码:', self.password_edit)
        
        folder_layout = QHBoxLayout()
        self.folder_edit = QLineEdit()
        self.folder_edit.setText(UPLOAD_FOLDER)
        self.browse_btn = QPushButton('浏览...')
        self.browse_btn.setObjectName("browseBtn")
        self.browse_btn.clicked.connect(self.browse_folder)
        folder_layout.addWidget(self.folder_edit)
        folder_layout.addWidget(self.browse_btn)
        auth_layout.addRow('上传文件夹:', folder_layout)
        
        auth_group.setLayout(auth_layout)
        main_layout.addWidget(auth_group)
        
        # 功能选项分组
        features_group = QGroupBox("功能选项设置")
        features_layout = QGridLayout()
        features_layout.setSpacing(10)
        
        self.download_check = QCheckBox("允许文件下载")
        self.download_check.setChecked(enable_file_download)
        features_layout.addWidget(self.download_check, 0, 0)
        
        self.upload_check = QCheckBox("允许文件上传")
        self.upload_check.setChecked(enable_file_upload)
        features_layout.addWidget(self.upload_check, 0, 1)
        
        self.video_check = QCheckBox("启用视频功能")
        self.video_check.setChecked(enable_video)
        features_layout.addWidget(self.video_check, 1, 0)
        
        self.control_check = QCheckBox("启用控制功能")
        self.control_check.setChecked(enable_control)
        features_layout.addWidget(self.control_check, 1, 1)
        
        features_group.setLayout(features_layout)
        main_layout.addWidget(features_group)

        # 基础显示控件（可复制但不可修改）
        display_group = QGroupBox("网络地址显示")
        display_layout = QVBoxLayout()
        display_group.setFixedHeight(130)
        
        # 创建只读文本编辑框
        self.info_display = QTextEdit()
        self.info_display.setReadOnly(True)  # 设置为只读
        self.info_display.setFixedHeight(40)
        
        # 复制按钮
        copy_layout = QHBoxLayout()
        self.copy_btn = QPushButton("复制内容")
        self.copy_btn.setObjectName("copyBtn")
        self.copy_btn.clicked.connect(self.copy_content)
        copy_layout.addWidget(self.copy_btn)
        copy_layout.addStretch()
        
        display_layout.addWidget(self.info_display)
        display_layout.addLayout(copy_layout)
        display_group.setLayout(display_layout)
        main_layout.addWidget(display_group)

        # 启动按钮
        self.status_label = QLabel('')
        self.status_label.setAlignment(Qt.AlignCenter)
        self.status_label.setStyleSheet("""
            color: #2c3e50;
            padding: 8px;
            border-radius: 4px;
            background-color: #ecf0f1;
        """)
        main_layout.addWidget(self.status_label)
        
        btn_layout = QHBoxLayout()
        btn_layout.setSpacing(15)
        
        self.save_btn = QPushButton('保存配置')
        self.save_btn.clicked.connect(self.save_config_txt)
        btn_layout.addWidget(self.save_btn)
        
        self.start_btn = QPushButton('启动程序')
        self.start_btn.setObjectName("startBtn")
        self.start_btn.clicked.connect(self.start_program)
        btn_layout.addWidget(self.start_btn)
        
        btn_layout.addStretch()
        main_layout.addLayout(btn_layout)
        
    def browse_folder(self):
        folder = QFileDialog.getExistingDirectory(self, "选择上传文件夹", UPLOAD_FOLDER)
        if folder:
            self.folder_edit.setText(folder)

    def save_config(self):
        global TARGET_FPS, MAX_WIDTH, STUN_PORT, WEB_PORT, users
        global enable_file_download, enable_file_upload, enable_video
        global enable_control, enable_ipv4, enable_ipv6, UPLOAD_FOLDER
        
        # 从文本框获取值时需要转换为整数
        try:
            TARGET_FPS = int(self.fps_edit.text())
            MAX_WIDTH = int(self.width_edit.text())
            STUN_PORT = int(self.stun_port_edit.text())
            WEB_PORT = int(self.web_port_edit.text())
            
            users = (self.username_edit.text(), self.password_edit.text())
            enable_file_download = self.download_check.isChecked()
            enable_file_upload = self.upload_check.isChecked()
            enable_video = self.video_check.isChecked()
            enable_control = self.control_check.isChecked()
            enable_ipv4 = self.ipv4_check.isChecked()
            enable_ipv6 = self.ipv6_check.isChecked()
            UPLOAD_FOLDER = self.folder_edit.text()
            
            self.status_label.setStyleSheet("""
                color: #27ae60;
                padding: 8px;
                border-radius: 4px;
                background-color: #eafaf1;
                font-weight: bold;
            """)
        except ValueError:
            self.status_label.setText('输入错误，请检查数字参数')
            self.status_label.setStyleSheet("""
                color: #e74c3c;
                padding: 8px;
                border-radius: 4px;
                background-color: #fdedeb;
                font-weight: bold;
            """)

    def save_config_txt(self):
        self.save_config()
        config_file = open('config.txt','w')
        config_file.write(f'''
            TARGET_FPS = {TARGET_FPS}
            MAX_WIDTH = {MAX_WIDTH}
            STUN_PORT = {STUN_PORT}
            WEB_PORT = {WEB_PORT}
            users = (\"{users[0]}\", \"{users[1]}\")
            enable_file_download = {enable_file_download}
            enable_file_upload = {enable_file_upload}
            enable_video = {enable_video}
            enable_control = {enable_control}
            enable_ipv4 = {enable_ipv4}
            enable_ipv6 = {enable_ipv6}
            UPLOAD_FOLDER = \"{UPLOAD_FOLDER}\"
        '''.replace(' ','').strip('\n'))
        config_file.close()
        self.status_label.setText('配置已保存')

    def start_program(self):
        # 先尝试保存配置
        try:
            # 验证输入是否有效
            int(self.fps_edit.text())
            int(self.width_edit.text())
            int(self.stun_port_edit.text())
            int(self.web_port_edit.text())
            
            self.save_config()
            self.status_label.setText('程序正在启动...')
            self.status_label.setStyleSheet("""
                color: #f39c12;
                padding: 8px;
                border-radius: 4px;
                background-color: #fef5e7;
                font-weight: bold;
            """)
            threading.Thread(target=main, daemon=True).start()
            self.status_label.setText('程序已启动')
            self.status_label.setStyleSheet("""
                color: #27ae60;
                padding: 8px;
                border-radius: 4px;
                background-color: #eafaf1;
                font-weight: bold;
            """)

            # 显示网络地址
            def get_address():
                result = subprocess.run(['ipconfig'], capture_output=True, text=True, check=True)
                lines = result.stdout.split('\n')
                address4 = '127.0.0.1'
                address6 = '[::1]'
                for line in lines:
                    if ('192.' in line) and ('IPv4 地址' in line):
                        address4 = line.split(': ')[-1].strip()
                    if '临时 IPv6 地址' in line:
                        address6 = '['+line.split(': ')[-1].strip()+']'
                return [address4,address6]
            address = get_address()
            self.set_display_text('https://' + address[0] + ':' + str(WEB_PORT) + '\n'+
                                  'https://' + address[1] + ':' + str(WEB_PORT))

        except ValueError:
            self.status_label.setText('输入错误，无法启动程序')
            self.status_label.setStyleSheet("""
                color: #e74c3c;
                padding: 8px;
                border-radius: 4px;
                background-color: #fdedeb;
                font-weight: bold;
            """)

    # 设置显示内容的方法
    def set_display_text(self, text):
        self.info_display.setText(text)
    
    # 追加显示内容的方法
    def append_display_text(self, text):
        self.info_display.append(text)
    
    # 复制当前显示的内容
    def copy_content(self):
        text = self.info_display.toPlainText()
        if text:
            clipboard = QApplication.clipboard()
            clipboard.setText(text)
            self.status_label.setText('内容已复制到剪贴板')
            self.status_label.setStyleSheet("""
                color: #f39c12;
                padding: 8px;
                border-radius: 4px;
                background-color: #fef5e7;
                font-weight: bold;
            """)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = ConfigWindow()
    window.show()
    sys.exit(app.exec_())
